![]() |
|
|||||
Gerade der letzte Punkt ist für Klassenerweiterungen erforderlich, allerdings auch nicht einfach zu realisieren. Serialisierungs-FrameworkDas Framework besteht aus zwei Teilen (Abb. 12.1).
Serialisierung beruht auf Im Unterschied zum allgemeinen I/O- bzw. Streaming-Konzept (vgl. dazu Kapitel 11, Package java.io) beruht die Objekt-Kommunikation auf Interfaces. Somit ist auch eine komplett eigencodierte Serialisierung möglich.
12.2.1 Standard-Serialisierung
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Um ein Objekt zu serialisieren, wird es der Instanz-Methode writeObject() von ObjectOutputStream übergeben. |
| Um ein neues Objekt - eine Art von Klon - auf der anderen Seite des Kanals zu deserialisieren, wird es der Instanz-Methode readObject() von ObjectInputStream übergeben (siehe Abb. 12.2). |
|
Zum Serialisieren verwendet man Referenzen der Interfaces ObjectOutput bzw. ObjectInput:
Standard-Code-Muster für Objekt-Übertragung
// Senden eines Objekts serObjectIn der Klasse SerClass ObjectOutput oos= new ObjectOutputStream(byteOutStream); oos.writeObject(serObjectIn);
// Empfang eines Objekts serObjectOut der Klasse SerClass ObjectInput ois= new ObjectInputStream(byteInStream); serObjectOut= (SerClass) ois.readObject();
Der Cast in der letzten Zeile ist notwendig, da readObject() ein Object zurückliefert.

Grundlegende Serialisierungs-Regeln
| 1. | Es werden nur Objekte serialisiert, keine statischen (Klassen-)Felder. |
| 2. | Die Klassen dieser Objekte müssen eines der Interfaces Serializable oder Externalizable implementieren. |
| 3. | Findet die Kommunikation zwischen zwei unterschiedlichen Prozessen statt, müssen beide JVMs Zugriff auf die Klasse des Objekts haben, da kein Byte-Code der Klasse übertragen wird. |
Zu 1: Ein Objekt kann nur deserialisiert werden, wenn seine Klasse geladen ist. Dann sind aber bereits die statischen Felder der Klasse initialisiert.
Zu 2: Bei Serializable handelt es sich um ein Marker-Interface.
Zu 3: Bei unterschiedlichen Prozessen, z.B. Netzwerkübertragung von Objekten, muss sichergestellt sein, dass die andere JVM ebenfalls Zugriff auf (kompatible) Klassen der übertragenen Objekte hat.
Serialisieren von Daten primitiver Typen
Neben Objekten können mit Hilfe der Methoden write<X>()1 und read<X>() der Interfaces DataOutput und DataInput auch primitive Typen übertragen werden (siehe Abb. 12.1 und 12.2).
Selbst wenn nur Daten primitiver Typen übertragen werden, beruht die Serialisierung auf einem vereinbarten Protokoll.
Serialisierungs-Protokoll mit Header
| Das Serialisierungs-Protokoll legt anhand einer Grammatik den Aufbau eines Byte-Streams fest. |
Den Anfang eines Byte-Streams bildet ein Header, der aus der short-Konstanten STREAM_MAGIC (0xaced) und der Version STREAM_VERSION (zurzeit aktuell 0x0005) besteht (siehe Beispiel in 12.2.2).
Block-Data-Record für write<X>-Methoden
Die mittels write<X>() übertragenen Daten werden in einem Block, dem so genannten Block-Data-Record, mit Kennung und Länge übertragen.
Die verwendeten Symbole sind im Interface ObjectStreamConstants (Abb. 12.1) definiert.
Wie bereits bei der einfachen I/O kann es leider auch hier noch zu Übertragungsfehlern, d.h. Fehlinterpretationen der Daten kommen:
Fehlinterpretationen nicht ausgeschlossen
| Das Protokoll für die Serialisierung von Daten primitiver Typen mittels write<X>() bzw. read<X>() lässt keine zweifelsfreie Rekonstruktion zu. |
Um Interna der Serialisierung besser zu verstehen, benötigt man eine Hex/ASCII-Darstellung der Byte-Streams. Hierzu verwenden wir im Folgenden die statische Methode toHexAsciiString() der Utility-Klasse Sniffer:
Sniffer:
Byte-Arrays in Hex/ASCII-
Darstellung
class Sniffer { private static char hex[]= {'0','1','2','3','4','5','6','7', '8','9','a','b','c','d','e','f'};
public static String toHexAsciiString (byte[] b,int lfAtPos){ if (b==null || b.length==0) return ""; StringBuffer sb= new StringBuffer(4*b.length); int i,r; for (i= 0; i<b.length;i++) { sb.append(hex[(b[i]>>>4)&0xF]).append(hex[b[i]&0xF]) .append(' '); if((i+1)%lfAtPos==0) { for (int j=i-lfAtPos+1; j<=i; j++) { if (32<=b[j] && b[j]<=126) sb.append((char)b[j]); else sb.append('.'); } sb.append('\n'); } } if (0 < (r= b.length%lfAtPos)) { for (i= 0;i<(lfAtPos-r)*3;i++) sb.append(' '); for (i= b.length-r;i<b.length; i++) { if (32<=b[i] && b[i]<=126) sb.append((char)b[i]); else sb.append('.'); } } else sb.deleteCharAt(sb.length()-1); return sb.toString(); } }
Die folgende Kommunikation mit ByteArrayOutputStream und ByteArrayInputStream ist rein speicherbasierend, sehr schnell und aus zwei Gründen interessant:
| Sie lässt die Analyse der Struktur des Byte-Streams durch die Sniffer-Klasse sehr einfach zu. |
| Sie erlaubt ein einfaches Cloning im Sinne von Deep-Copy (siehe auch 6.10.2). |
Byte-Stream eines Block-Data-Records
public class Test { public static void main(String[] args) { byte[] barr= null; ByteArrayOutputStream baos= new ByteArrayOutputStream();
try { // schreibt bereits Header, siehe Erklärung unten
ObjectOutput oout= new ObjectOutputStream(baos); ¨
oout.writeInt(7); oout.writeBoolean(false); oout.write((byte)49); oout.flush(); // schreibt nach baos
// wandelt Byte-Stream in Byte-Array um barr= baos.toByteArray(); // Hex/ASCII-Darstellung mit 16 Zeichen pro Zeile System.out.println(Sniffer.toHexAsciiString(barr,16));
ObjectInput oin= new ObjectInputStream( new ByteArrayInputStream(barr)); System.out.println(oin.readInt()); System.out.println(oin.readByte()); ¦ System.out.println(oin.readBoolean()); Æ // System.out.println(oin.readChar()); Ø
} catch (IOException e) { System.out.println(e); } } }
Hex/ASCII:
Header, Block-Kennung und -Länge, Daten primitiver Typen
ac ed 00 05 77 06 00 00 00 07 00 31 ....w......1 7 0 true |
Zu ¨: Wie am Anfang dieses Abschnitts beschrieben, bilden die ersten vier Bytes den Header. Dieser wird bereits bei der Anlage des ObjectOutputStream mit writeStreamHeader() herausgeschrieben. Deshalb muss die Anweisung im try-catch stehen.
Zum Block-Data-Record: Der Block-Anfang ist mit 77 (TC_BLOCKDATA) markiert, 06 ist dann die Block-Länge und anschließend folgen die Daten.
Zu write<X>, read<X>: Typ-Informationen sind im Stream nicht enthalten. Deshalb können die Zeilen ¦ und Æ z.B. auch durch die Zeile Ø ersetzt werden, ohne dass dieser Fehler erkannt werden kann. Dies würde dann das Zeichen 1 liefern.
| Grundsätzlich werden alle Zeichen vom Typ char oder String als UTF8 im Stream codiert. |
Für Objekte ist das Protokoll sowie das Stream-Format verständlicherweise wesentlich komplexer.
Mit den Interfaces Serializable und Externalizable sind zwei unterschiedliche Protokolle verbunden, die festlegen wie Objekte übertragen werden. Zusätzlich zu den bereits o.a. grundsätzlichen Regeln gilt:
| 1. | Implementiert eine Klasse keine der beiden Interfaces, so führt der Versuch, ein Objekt dieser Klasse zu serialisieren, zu der Ausnahme NotSerializableException. |
| 2. | Für die Klassen, die Externalizable implementieren, überträgt das Protokoll nur den voll qualifizierten Klassennamen. Alle weiteren Informationen zu den Feldern müssen von der Klasse selbst im Byte-Stream mittels der beiden Methoden |
| public void writeExternal(ObjectOutput out) |
| public void readExternal(ObjectInput in) |
Default-Serialisierung: Klassen
implementieren nichts außer Serializable
| 3. | Für Klassen, die nur Serializable implementieren, legt das Protokoll automatisch die Reihenfolge und Identifizierung seiner Felder und seiner Superklassen fest, wobei |
| static deklarierte Klassen-Variablen nicht serialisiert werden |
| die Klasse die zu übertragenden Instanz-Felder selbst bestimmen kann |
Da Externalizable das Interface Serializable erweitert (Abb. 12.1), ist dass »nur« in der letzten Regel wesentlich. Denn alle Klassen, die Externalizable implementieren, sind auch als Serializable markiert.
Die zweite und dritte Regel hat eine wichtige Implikation:
Externalizable Klassen:
Einbahnstraße für Subklassen
| Für die Subklassen einer Klasse, die Externalizable implementiert, gibt es keine automatische Serialisierung mehr. |
Der folgende Code ist syntaktisch korrekt, aber semantisch sinnlos:
class E implements Externalizable { /*...*/ }
class D extends E implements Serializable { /*...*/ }
Die Klasse D muss trotzdem die Serialisierung selbst übernehmen.
Das Interface Externalizable deklariert zwei Methoden, die jeweils als Argument einen Object-Stream übergeben bekommen.
Externalizable:
kein Marker- Interface
public interface Externalizable extends Serializable { void writeExternal(ObjectOutput out) throws IOException; void readExternal (ObjectInput in) throws IOException, ClassNotFoundException; }
Aufgrund der Object-Streams out bzw. in stehen bei der Implementierung der Methoden somit alle Methoden zum Lesen und Schreiben von primitiven Typen und Objekten zur Verfügung.
Anforderungen an externalizable Klassen
Klassen, die bei der Objekt-Kommunikation eine vollständige Kontrolle über die Feld-Inhalte benötigen, müssen
| Externalizable mit den beiden Methoden writeExternal() bzw. readExternal() implementieren. |
| einen public deklarierten No-Arg-Konstruktor 2 enthalten. |
Im Gegensatz zu rein Serializable-Klassen kann eine Externalizable-Klasse von außen beliebig instanziiert werden, was für das Design recht unangenehme Konsequenzen haben kann (siehe auch 12.4.2).
Protokoll-Ablauf bei Externalizable
Da die Hauptarbeit der Objekt-Kommunikation bis auf den Klassennamen nicht automatisch erfolgt, ist das Protokoll eigentlich recht einfach.
| 1. | der voll qualifizierte Klassenname des Objekts ASCII-codiert (nicht in Unicode!) in den Stream geschrieben.3 |
| 2. | die Methode writeExternal() der Klasse mit dem entsprechenenden ObjectOutput-Stream aufgerufen. |
Bei der Deserialisierung wird umgekehrt
| 1. | die Klasse anhand des im Stream enthaltenen Namens identifiziert. |
| 2. | eine Instanz der Klasse mit Hilfe des public No-Arg-Konstruktors erschaffen. |
| 3. | die Methode readExternal() der Klasse mit dem entsprechenenden ObjectInput-Stream aufgerufen. |
Beliebige Manipulation der Feld-Informationen
Mit writeExternal() werden die gewünschten Felder des Objekts in den Stream geschrieben, wobei beliebige Manipulationen, z.B. Verschlüsselung der Daten, möglich sind.
Sollen interne Objekte übertragen werden, muss dies selbst kodiert werden.
Reihenfolge der Felder ist wichtig
Bei readExternal() ist dann darauf zu achten, dass die Reihenfolge der Felder exakt eingehalten wird und eventuelle Entschlüsselungen vorgenommen werden.
Externalizable:
Felder von Superklassen nicht implizit enthalten
| Die Felder aller Superklassen - selbst wenn diese als Serializable markiert sein sollten - müssen ebenfalls explizit übertragen werden. |
Gemäß dem o.a. zweiten Punkt der Deserialisierung, werden zwar automatisch die Felder der Superklassen eines Objekts angelegt, dies sorgt aber nur für die Default-Initialisierung.
Enthält das zu übertragende Objekt davon abweichende Werte, sind diese ohne explizite Übertragung im deserialisierten Objekt nicht vorhanden.
Nachfolgend wird eine sehr einfache Klasse E angelegt:
package kap12; import java.io.*;
Beispiel:
externalizable Klasse
class E implements Externalizable { byte b= 1;
public E() {}; // public notwendig! E(byte b) { this.b= b; }
public void writeExternal (ObjectOutput out) { try { out.writeByte(b); } catch (Exception e) {System.out.println(e);} }
public void readExternal (ObjectInput in) { try { b= (byte) in.readByte(); } catch (Exception e) {System.out.println(e);} } }
Ein Objekt der Klasse E wird nun wieder (de-)serialisiert und der Byte-Stream mit Sniffer ausgegeben:
public class Test { public static void main(String[] args) { ObjectOutput oout= new ObjectOutputStream(baos); oout.writeObject(new E((byte)3)); oout.flush();
barr= baos.toByteArray(); System.out.println(Sniffer.toHexAsciiString(barr,16));
ObjectInput oin= new ObjectInputStream( new ByteArrayInputStream(barr)); System.out.println(((E)oin.readObject()).b); } }
Beispiel:
Protokoll- Bytes einer externalizable Klasse
ac ed 00 05 73 72 00 07 6b 61 70 31 32 2e 45 c1 ....sr..kap12.E. b5 79 8a 99 51 65 29 0c 00 00 78 70 77 01 03 78 .y..Qe)...xpw..x 3 |
Erklärung: Nach dem obligatorischen 4-Byte-Header startet das Objekt im Stream mit TC_OBJECT (73).
Nach der Klassenkennung TC_CLASSDESC(72) folgt die Länge (0007) und der Name der Klasse (kap12.E) als ASCII-Zeichen.
Anschließend folgt ein 64-Bit-Hashcode (c1..29), der die Klassen eindeutig identifiziert.
Am Ende stehen dann die Feldwerte des Objekts in einem Block. In diesem Fall ist dies nur ein Byte (03), eingerahmt in TC_BLOCKDATA(77), Länge (01) und TC_ENDBLOCKDATA(78).
Externalizable:
eigenes Protokoll zu Feldern
Auch bei Externalizable ist das Protokoll bis auf die eigentlichen Feld-Informationen vorgegeben. Die Daten der Felder sind aber frei manipulierbar.
Implementiert eine Klasse Serializable, so kann die Serialisierung völlig transparent von der JVM übernommen werden.
Allerdings stellt das Protokoll gewisse Anforderungen an die Objekte und ihre Klassen.
Anforderungen an serializable Klassen
Eine Klasse ist serializable4 , wenn
| keine ihrer Superklassen Externalizable implementiert und |
| sie Zugriff auf den No-Arg-Konstruktor der ersten Superklasse hat, die nicht serialisierbar ist.5 |
Anforderungen an serializable Objekte
Ein Objekt ist dann serializable, wenn
| seine Klasse serializable ist und |
| seine Referenz-Felder entweder null sind oder Objekte von Klassen referenziert, die serialisierbar sind. |
Unter dem Begriff Default-Serialisierung versteht man das Standard-Protokoll einer serializable Klasse, die selbst keine Serialisierungs-Methoden implementiert.
It´s magic -
read/writeObject(): Zugriff auf private Felder
Das Standard-Protokoll verwendet hierzu writeObject() bzw. readObject() von ObjectOutputStream bzw. ObjectInputStream.
| Die Methoden writeObject() und readObject() haben dazu »magischen« Zugriff auch auf alle private deklarierten Felder der Objekte. |
Ablauf der Default-
Serialisierung
Der Serialisierungs-Prozess eines Objekts mit der Methode writeObject() läuft im Wesentlichen wie folgt ab6 :
| 1. | Es werden der Klassen-Deskriptor, d.h. der Klassenname sowie die Namen aller nicht transient deklarierten Felder in den Stream geschrieben. |
| 2. | Die Werte der Felder werden mit der Methode defaultWriteObject() in den Stream geschrieben. |
| 3. | Dabei werden alle Objekte, die über Felder vom Objekt erreichbar sind, ebenfalls in den Stream geschrieben, wobei für Objekte, die bereits serialisiert sind, nur ein Handle abgelegt wird. Diese Operation wird kurz als transitive Hülle (transitive closure) bezeichnet. |
| 4. | Ist ein Objekt Instanz einer serializable Subklasse, wird für die serialisierbaren Superklassen ebenfalls writeObject() oder es werden ihre speziellen Serialisierungs-Methoden7 aufgerufen. |
Im Gegensatz zu Externalizable werden also zu jedem Objekt auch die Namen der übertragenen Felder im Stream geschrieben.
Zu Klassen- bzw. Feldbeschreibungen wird eine Instanz des Klassen-Deskriptors ObjectStreamClass serialisiert.
Standard-Objekte wie Object oder String werden allerdings nur als Ident in den Stream geschrieben (siehe Konstanten in ObjectStreamConstants).
Beispiel:
Klassen- Hierarchie mit Aggregation
Objekte der folgenden Klasse Square sind serializable, unabhängig davon, ob die Superklasse Figure serialisierbar ist oder nicht.
Point muss allerdings in jedem Fall serialisierbar sein (vgl. 12.3.4 »Anforderungen an ein Objekt«).
|
class Point implements Serializable { int x,y; }
class Figure /* implements Serializable */ { ¨ protected Point base= new Point(); public Figure() { this(0,0); } public Figure(int x, int y) { base.x=x; base.y=y; } }
class Square extends Figure implements Serializable { private Point p= new Point(); transient public boolean clockwise= true; public Square (int x1, int y1, int x2, int y2) { super(x1,y1); p.x= x2; p.y= y2; } public String toString() { return "["+ base.x+","+ base.y+";"+p.x+","+p.y+";"+ clockwise+"]"; } }
Zu ¨: Klassen- bzw. Feld-Informationen und Werte zu Figure werden in den Stream nur aufgenommen, wenn die Klasse serialisierbar ist, z.B. Serializable implementiert.
Ablauf
der Default- Deserialisierung
Der Deserialisierungs-Prozess der Methode readObject() für ein Objekt läuft prinzipiell wie folgt ab:
| 1. | Nach Deserialisierung der ObjectStreamClass-Instanz werden die Klassen- bzw. Feldbeschreibungen ausgewertet und die zugehörige Klasse wird im lokalen System geladen. |
| 2. | Es wird eine Instanz erzeugt. |
| Für externalizable Objekte wird der public No-Arg-Konstruktor aufgerufen und anschließend readExternal(). |
| Für serializable Objekte wird - falls notwendig - der No-Arg-Konstruktor der ersten nicht serialisierbaren Superklasse aufgerufen. |
| 3. | Die Felder werden ohne Aufruf eines Konstruktors oder Instanz-Initialisierers mit Hilfe der Methode defaultReadObject() mit Werten belegt. |
| Felder, für die es keinen Wert im Stream gibt, erhalten den Default-Wert. |
Die neu erschaffene Instanz ist somit total unabhängig vom originalen Objekt. Abschließend noch eine Anmerkung:
| Der Versuch, ein Objekt als primitiven Typ zu deserialisieren, führt zu der Ausnahme EOFException. |
Die Ausgabe von Test hängt also davon ab, ob Figure (siehe oben, Zeile ¨) das Interface Serializable implementiert hat.
public class Test { public static void main(String[] args) { ByteArrayOutputStream baos= new ByteArrayOutputStream(); ObjectOutput oout= new ObjectOutputStream(baos); oout.writeObject(new Square(1,2,3,4)); oout.flush();
ObjectInput oin= new ObjectInputStream( new ByteArrayInputStream( baos.toByteArray())); System.out.println(((Square)oin.readObject())); // Figure nicht serialisierbar :: [0,0;3,4;false]
// Figure serialisierbar :: [1,2;3,4;false]
} }
Anpassung der Serialisierungs-Mechanismen
Neben den Varianten Default-Serialisierung bzw. Serialisierung mittels Externalizable gibt es noch weitere - teilweise recht komplexe - Möglichkeiten der Protokoll-Anpassung.
serialPersistentFields überschreibt transient
Die Kennzeichnung der nicht serialisierbaren Instanz-Felder als transient ist zwar einfach, aber manchmal nicht flexibel genug.8
Dieser Default-Mechanismus lässt sich mit Hilfe des speziellen private static final deklarierten Felds serialPerstistentFields innerhalb der serializable Klasse überschreiben.
serialPersistentFields: ein Code-Muster
| Das Feld serialPerstistentFields muss mit einem Array von ObjectStreamField-Instanzen initialisiert werden, die die Namen und Typen der serialisierbaren Instanz-Felder enthalten: |
class C implements Serializable { // ..Felder.. private static final ObjectStreamField[] serialPersistentFields = { new ObjectStreamField("fieldname", fieldType.class), //... }; //... }
In der Klasse C werden trotz transient die Felder s und i serialisiert:
class C implements Serializable { transient String s= "abc"; // transient nutzlos transient int i= 2; // dito
private static final ObjectStreamField[] serialPersistentFields = { new ObjectStreamField("s", String.class), new ObjectStreamField("i", int.class) }; }
Keine Kapselung: gravierender Nachteil von Externalizable
Ein gravierender Nachteil der Implementierung von Externalizable liegt darin, dass die Methoden writeExternal() bzw. readExternal() public deklariert werden müssen.
| Die Externalizable-Methoden können nicht nur von der JVM zur Serialisierung, sondern für jeden anderen Zweck missbraucht werden. |
Aus Sicht der Kapselung sind Anpassungen bzw. Erweiterungen des Default-Mechanismus sicherlich die bessere Wahl.
Klassenintern read/write Object() überschreiben Default-Mechanismus
Mit Hilfe der klasseninternen private deklarierten Methoden writeObject() und readObject() kann eine serializable Klasse den Default-Mechanismus anpassen.
Dazu muss die Klasse diese Methoden mit folgender Signatur (mit oder ohne throws) implementieren:
class C implements Serializable {
defaultWriteObject(): Aufruf des Defaults
private void writeObject (ObjectOutputStream oout) throws IOException { //... // oout.defaultWriteObject(); //... }
private void readObject (ObjectInputStream oin) throws ClassNotFoundException, IOException {
defaultRead
Object(): Aufruf des Defaults
//... // oin.defaultReadObject(); //... } }
Diese beiden Methoden sind wie ihre gleichnamigen Pendants in den beiden Object-Streams magisch.9
| Da ObjectOutputStream bzw. ObjectInputStream als Argument übergeben wird, können mit Hilfe der Default-Methoden die Werte der »normalen« Felder serialisiert werden. |
Die Klasse C enthält jeweils drei Varianten zu writeObject() bzw. readObject(). Beide Methoden
read/writeObject():
drei interne Varianten
| 1. | sind leer, d.h. ohne Anweisung. |
| 2. | rufen nur ihre Default-Methoden auf (zusätzlich Zeile ¨ bzw. Ø). |
| 3. | rufen ihre Default-Methoden auf und setzen Datum und Zeit des statischen Felds d (zusätzlich Zeile ¨ ¦ bzw. Æ Ø). |
class C implements Serializable { static Date d; String s= "hi"; int i= 7; private void writeObject (ObjectOutputStream oout) { try { // oout.defaultWriteObject(); ¨ // oout.writeObject(Calendar.getInstance().getTime()); ¦ } catch (Exception e) {System.out.println(e);} } private void readObject (ObjectInputStream oin) { try { // oin.defaultReadObject(); Æ // d= (Date) oin.readObject(); Ø } catch (Exception e) {System.out.println(e);} } public String toString() { return d+","+s+","+i; } }
public class Test { public static void main(String[] args) { byte[] barr= null; ByteArrayOutputStream baos= new ByteArrayOutputStream(); ObjectOutput oout= new ObjectOutputStream(baos); oout.writeObject(new C()); oout.flush();
barr= baos.toByteArray(); System.out.println(Sniffer.toHexAsciiString(barr,16)); ObjectInput oin= new ObjectInputStream( new ByteArrayInputStream( baos.toByteArray())); System.out.println(oin.readObject()); } }
Es werden nur die Klassen- und Feld-Informationen übertragen, da eine Instanz der ObjectStreamClass serialisiert wird.
Es fehlen alle Werte. Die Ausgabe ist uninteressant und wird deshalb weggelassen.
Mit mehr Aufwand hat man praktisch die Default-Serialisierung nachgebildet. Auch der Stream-Inhalt ist nahezu identisch mit demjenigen der Default-Serialisierung.
ac ed 00 05 73 72 00 07 6b 61 70 31 32 2e 43 2c ....sr..kap12.C, 92 61 0f 51 e2 bc ff 03 00 02 49 00 01 69 4c 00 .a.Q......I..iL. 01 73 74 00 12 4c 6a 61 76 61 2f 6c 61 6e 67 2f .st..Ljava/lang/ 53 74 72 69 6e 67 3b 78 70 00 00 00 07 74 00 02 String;xp....t.. 68 69 78 hix null,hi,7 |
Es wurde nur das Stream-Flag SC_SERIALIZABLE (0x02) mit dem Stream-Flag SC_WRITE_METHOD (0x01) (per AND) zu 0x03 überlagert. Ein zusätzliches TC_ENDBLOCKDATA (0x78) terminiert die Werte.
Zusatz-
Funktionalität: Serialisieren von statischen Feldern
Eine zusätzliche Funktionalität ist eigentlich der Sinn der klasseninternen Implementation der beiden Methoden.
| In diesem Fall wird der Wert eines statischen Felds übertragen, was bei der Default-Serialisierung nicht möglich ist.10 |
Thu Dec 07 20:31:34 GMT+01:00 2000,hi,7 |
Die Ausgabe zeigt die Übertragung und erfolgreiche Initialisierung des statischen Felds d, das in der zweiten Variante noch null war.
Serialisierung ist ein zusätzlicher Dienst zu einer Klasse. Es ist nicht unbedingt vorteilhaft, diesen Dienst in der Klasse selbst zu implementieren.
Broker-Pattern: transparentes Service-Objekt
Der nachfolgende Mechanismus basiert auf dem Broker-Pattern.
| Ein Broker (Objekt-Makler) ist ein Service-Objekt, das für eine Klasse einen bestimmten Dienst transparent für die Clients abwickelt. |
Im konkreten Fall wird der Serialisierungs-Dienst der Klasse nicht von ihr selbst abgewickelt, sondern einem Broker-Objekt überlassen.
| Es wird also kein Objekt der serializable Klasse selbst, sondern ein Broker-Objekt in den Stream geschrieben (Abb. 12.4). |
Broker-Muster für die Serialisierung
|
Die Klasse selbst wird vom Serialisierungs-Prozess entlastet. Die Aufgabe der Übernahme relevanter Objekt-Informationen sowie der Wiederherstellung des Objekts liegt ausschließlich beim Broker-Objekt.
Ohne die ursprüngliche Klasse ändern zu müssen, können in Broker-Objekten verschiedene Strategien der Übertragung angewendet werden.
Broker-Delegation in der Server-Klasse:
writeReplace()
Damit der Broker-Mechanismus auch greift, muss die serialisierbare Klasse die folgende Methode (in der Regel transparent als private) implementieren:
[public|protected|private] Object writeReplace() throws ObjectStreamException;
Wird nun ein Objekt dieser Klasse durch ObjectOutputStream serialisiert, ruft dieser die Methode writeReplace() auf und schreibt das Resultat als Broker-Objekt in den Byte-Stream.11
| Da im Byte-Stream nur das Broker-Objekt enthalten ist, kann die Deserialisierung nur aus der Rekonstruktion des Broker-Objekts bestehen. |
Die delegierende Klasse kann zwar auch Externalizable implementieren, dies ist aber nicht unbedingt sinnvoll, da sie dann zwei Methoden enthält, die nicht genutzt werden.
Mechanismus in der Broker-Klasse:
readResolve()
Implementiert die serialisierbare Broker-Klasse die Methode
[public|protected|private] Object readResolve() throws ObjectStreamException;
so ruft ObjectInputStream beim Deserialisieren die Methode readResolve() des Broker-Objekts auf.
Das Resultat dieser Methode ist dann in der Regel das rekonstruierte Objekt des Originals (der delegierenden Klasse).
writeReplace() und readResolve() sind unabhängig
Die Methoden writeReplace() und readResolve() sind unabhängig voneinander, können somit auch einzeln eingesetzt werden.
Die Methode writeReplace() kann in einer serialisierbaren Klasse ohne Broker eingesetzt werden, um z.B. nur bestimmte Objekte der Klasse zu deserialisieren.
| Entgegen dem Eindruck der offiziellen Dokumentation kann das Broker-Objekt durchaus mit readResolve() ein beliebiges Objekt liefern, nicht unbedingt das der ursprünglichen Klasse. |
Broker-Lösung basiert nicht auf Interfaces
Der Broker-Mechanismus basiert nicht auf Interfaces, da die beiden Methoden dann nur public deklariert sein könnten.
Das aktuelle Interface-Konzept ist also - wieder einmal - nicht flexibel genug, d.h. wird hier durch Reflexion oder (magic) private-Zugriffe umgangen.
Die triviale Klasse C ist ihr eigener Broker, d.h., es wird nur wieder die Default-Serialisierung nachgebildet:
class C implements Serializable { private Object writeReplace() throws ObjectStreamException { return this; } private Object readResolve() throws ObjectStreamException { return this; } }
Delegator-Klasse mit zwei Broker-Varianten
Zur Klasse Delegator werden mögliche Broker-Varianten vorgestellt:
class Delegator implements Serializable { private int i; public Delegator(int i) { this.i= i; } public int geti() { return i; } private Object writeReplace() throws ObjectStreamException { return new Broker(this); } }
Der Broker deserialisiert einfach ein konstantes String-Objekt:
Deserialisierung eines konstanten String-Objekts
class Broker implements Serializable { public Broker(Delegator d) {} private Object readResolve() throws ObjectStreamException { return "Delegator?"; } }
Der Broker deserialisiert ein Delegator-Objekt im gleichen Zustand:
Deserialisierung eines Delegator-Objekts im gleichen Zustand
class Broker implements Serializable { private int i; public Broker(Delegator d) { i= d.geti()^0xaaaa; } private Object readResolve() throws ObjectStreamException { return new Delegator(i^0xaaaa); } }
Klassen-Evolution: Versionswechsel nach
Serialisierung
Die Objekt-Kommunikation kann bedingt durch Zeitversatz (Serialisieren in Dateien) oder verschiedene JVMs auf unterschiedliche Klassen-Versionen treffen.
Natürlich trifft dieses Problem gleichermaßen auf serializable und externalizable Klassen zu. Die folgende Diskussion beschränkt sich aber ausschließlich auf Klassen, die Serializable implementieren.
(S)UID: Stream Unique Identifier
Serialisierte Objekte einer anderen Klassen-Version können nicht ohne spezielle Vorkehrungen deserialisiert werden.
Eindeutiger Hashcode der Klassen-Version
| Mit jedem Objekt wird nicht nur die zugehörige Klasse, sondern auch ein eindeutiges Ident - kurz UID - vom Typ long serialisiert, das nicht nur die Klasse, sondern auch jede Version der Klasse eindeutig identifiziert. |
Das UID ist der Hashcode12 , berechnet aus allen relevanten Klassen-Informationen wie Name, Felder, Parameter, Modifier etc. und ändert sich somit mit jeder neuen Version.
| Serializable Klassen mit gleichem Namen, die dieselbe UID haben, werden bei der (De-)Serialisierung als stream-kompatible angesehen.13 |
Mit demselben UID wird ausgedrückt, dass die neue Klassen-Version den Client-Kontrakt der alten einhält (siehe Stream-Kompatibilität).
| Für stream-kompatible Versionen einer Klasse C kann man das UID manuell durch folgende Anweisung setzen: |
class C implements Serializable {
// jede stream-kompatible Version hat dieselbe id
public static final long serialVersionUID= idL; 14
//... }
Der Wert von id ist nicht festgelegt, er muss nur identifizierend sein.
Will man ein UID-konformes Ident vergeben, kann man diesen manuell durch das Utility-Programm serialver15 berechnen lassen oder im Programm mit Hilfe der Klasse ObjectStreamClass:
class C implements Serializable {
static public long uid() { return ObjectStreamClass.lookup(C.class) .getSerialVersionUID();
} //... }
In der Praxis wird bereits die erste Version einer serializable Klasse, nachdem sie stabil ist, manuell mit einer UID versehen. Denn für jede Klasse, die kein serialVersionUID deklariert hat, wird sonst vor dem Streaming-Prozess der Hashcode berechnet.
Der Begriff stream-kompatibel soll im Weiteren kurz präzisiert werden. Es gibt hierzu zwei Aspekte, d.h. eine notwendige und eine hinreichende Bedingung.
Notwendig für eine stream-kompatible Klassen-Evolution ist die Frage, was die Default-Serialisierung beim Deserialisierungs-Prozess an Versionsänderungen toleriert:
| Neue Felder: Sind Felder im Stream, d.h. in der alten Version, nicht enthalten, werden sie mit 0 oder null initialisiert (nicht mit ihren Initialisierungswerten!). |
| Fehlende Felder: Felder im Stream, die es in der neuen Version nicht mehr gibt, werden einfach ignoriert. |
Nicht toleriert, d.h. mit einer InvalidClassException bestraft, werden dagegen folgende Änderungen:
| Typ-Wechsel: Ein Feld mit gleichem Namen wechselt den Typ. |
| Änderung der Serialisierungsart: Eine Klasse, die im Stream enthalten ist, ändert ihre Serialisierungsart. |